﻿using System;
using System.Collections.Generic;
using System.Text;
using AdaptiveAgents.Loggers;
using AdaptiveAgents.Agents;
using AdaptiveAgents.Results;

namespace AdaptiveAgents.Games
{
    /// <summary>
    /// Represents a game that is played by a group of agents
    /// </summary>
    public abstract class Game
    {
        protected int _numOfRounds = 0; //Number of rounds of the game
        protected Environment _environment; //EnvironmentType where the game is played
        protected double _accumUtility; //Accumulated utility from the game
        protected double _currentUtility = 0;//Utility of a specific round in the game

        private const int PLAYER0 = 0;
        private const int DEFAULTPASS = -1;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="environment">EnvironmentType that contains the players</param>
        /// <param name="numOfRounds">Number of game rounds</param>
        public Game(Environment env, int numOfRounds)
        {
            this._environment = env;
            this._numOfRounds = numOfRounds;
        }

        /// <summary>
        /// Decides which player is the next player
        /// Set the method that the agent will choose who to pass to
        /// </summary>
        /// <param name="activePlayer"></param>
        /// <returns>ID of the next player</returns>
        protected virtual int chooseNextPerformer(int activePlayer)
        {
            return _environment.agents[activePlayer].chooseNextPerformer();
        }

        /// <summary>
        /// Decides which player is the next player with special rules if this is Player 0
        /// </summary>
        /// <param name="activePlayer"></param>
        /// <param name="whoToPassToIfThisIsPlayer0"></param>
        /// <returns>ID of the next player according to active player decision</returns>
        protected int chooseNextPerformer(int activePlayer, int whoToPassToIfThisIsPlayer0)
        {
            int next;
            switch (activePlayer)
            {
                case PLAYER0:
                    switch (whoToPassToIfThisIsPlayer0)
                    {
                        case DEFAULTPASS:
                            next = _environment.agents[activePlayer].chooseNextPerformer();
                            break;
                        default:
                            next = whoToPassToIfThisIsPlayer0;
                            break;
                    }                    
                    break;                
                default:
                    next = _environment.agents[activePlayer].chooseNextPerformer();
                    break;
            }
            return next;
        }
 
        /// <summary>
        /// Initialization of game environment and log game details
        /// </summary>
        /// <param name="logger">Logger for game details</param>
        /// <param name="numOfAgents">Number of players in game</param>
        protected virtual void initializeGame(Logger logger, int numOfAgents)
        {
            //Initialize teammate model in each agent
            foreach (Agent agent in _environment.agents)
                agent.initializeTeammateModels(numOfAgents);

            //Log: number of agents in environment
            logger.WriteLine(MessageType.PlayersCount, numOfAgents);

            //Log: agent attributes
            foreach (Agent agent in _environment.agents)
            {
                logger.WriteLine(MessageType.AgentAttributes, agent.Epsilon, agent.Competence);
            }

            //Log: new line
            logger.WriteLine(MessageType.LineSeperator, "");

        }

        /// <summary>
        /// Make a move by current active player.
        /// </summary>
        /// <param name="round">The round that current active player plays in</param>
        /// <param name="activePlayer">Current active player</param>
        /// <param name="turn">Used for keep tracking of turn data</param>
        /// <param name="accumUtility">Total utility until current turn</param>
        protected virtual void makeMove(int round, int activePlayer, Turn turn, ref double accumUtility)
        {
            //Get the performance of the active player
            double performance = _environment.agents[activePlayer].performNow();

            //Who is the active player
            turn.ActivePlayer = activePlayer;

            //Accumulate the performance of the active player
            accumUtility += performance;

            //Active player performance
            turn.Performence = performance;

            //Add a new observation of current turn by all players
            foreach (Agent agent in _environment.agents)
                agent.addObservation(new Observation(round, _environment.agents[activePlayer].ID, performance));

            //Estimate competence of each performer
            for (int i = 0; i < _environment.agents.Count; ++i)
            {
                int otherAgent = (i + 1) % _environment.agents.Count; //ID of other agent than current agent
                turn.addEstimatedCompetence(_environment.agents[otherAgent].getTeammateModel(i).EstimateCompetence());
            }
        }
    
        /// <summary>
        /// Start playing the game
        /// </summary>
        /// <param name="logger">Logger that monitors game activity</param>
        /// <returns>Average utility of the players</returns>
        public virtual double start(Logger logger)
        {
            int numOfAgents = _environment.agents.Count;
            
            logger.WriteLine(MessageType.GameDetails, numOfAgents, GameType.Normal.ToString());

            //Initialization of game environment
            initializeGame(logger, numOfAgents);

            _accumUtility = 0; //Total utility of all agents in group
            int nextActivePlayer;
            int activePlayer = Generator.getNextInt(numOfAgents); //Choose randomly the first agent to play
            
            //Play the game
            for (int round = 0; round < _numOfRounds; ++round)
            {
                //Create a turn record
                Turn turn = new Turn(numOfAgents);

                //Make a move by the active player
                makeMove(round, activePlayer, turn, ref _accumUtility);

                //Choose the next performer
                nextActivePlayer = chooseNextPerformer(activePlayer); 

                //Update the last observation of each player with the next player
                foreach (Agent agent in _environment.agents)
                {
                    bool actExpected = agent.updateLastObservationWithNextAgent(activePlayer, nextActivePlayer);
                    turn.addExpectedAct(actExpected);
                }
 
                //Set active player
                activePlayer = nextActivePlayer;
            
                //Log: turn data
                logger.WriteLine(MessageType.Turn, turn);
            }

            /***** Logging the agents' summaries *****/

            return logStatistics(logger);
        }

        /// <summary>
        /// Alternative - Start playing the game
        /// </summary>
        /// <param name="logger">Logger that monitors game activity</param>
        /// <returns>Average utility of the players</returns>
        public virtual double start(Logger logger, out ExperimentResults Results)
        {
            int numOfAgents = _environment.agents.Count;
            Results = new ExperimentResults();
            logger.WriteLine(MessageType.GameDetails, numOfAgents, GameType.Normal.ToString());

            //Initialization of game environment
            initializeGame(logger, numOfAgents);

            _accumUtility = 0; //Total utility of all agents in group
            _currentUtility = 0;
            int nextActivePlayer;
            int activePlayer = Generator.getNextInt(numOfAgents); //Choose randomly the first agent to play

            //Play the game
            for (int round = 0; round < _numOfRounds; ++round)
            {
                //Create a turn record
                Turn turn = new Turn(numOfAgents);

                //Make a move by the active player
                makeMove(round, activePlayer, turn, ref _accumUtility);

                //Choose the next performer
                nextActivePlayer = chooseNextPerformer(activePlayer);

                //Update the last observation of each player with the next player
                foreach (Agent agent in _environment.agents)
                {
                    bool actExpected = agent.updateLastObservationWithNextAgent(activePlayer, nextActivePlayer);
                    turn.addExpectedAct(actExpected);
                }

                //Handle the results file
                Results.IncreasePlayerNumberOfTurnsPlayed(activePlayer);
                Results.IncreasePlayerAccumilatingProfit(activePlayer, _accumUtility-_currentUtility);
                _currentUtility = _accumUtility;
                //Set active player
                activePlayer = nextActivePlayer;

                //Log: turn data
                logger.WriteLine(MessageType.Turn, turn);
            }

            /***** Logging the agents' summaries *****/

            return logStatistics(logger);
        }

        /// <summary>
        /// Alternative For Deterministic game - Start playing the game
        /// </summary>
        /// <param name="logger">Logger that monitors game activity</param>
        /// <returns>Average utility of the players</returns>
        public virtual double start(Logger logger, out ExperimentResults Results, int inPlayer0ChoiceOfNextPlayer)
        {
            int numOfAgents = _environment.agents.Count;
            Results = new ExperimentResults();
            logger.WriteLine(MessageType.GameDetails, numOfAgents, GameType.Normal.ToString());

            //Initialization of game environment
            initializeGame(logger, numOfAgents);

            _accumUtility = 0; //Total utility of all agents in group
            _currentUtility = 0;
            int nextActivePlayer;
            int activePlayer = Generator.getNextInt(numOfAgents); //Choose randomly the first agent to play

            //Play the game
            for (int round = 0; round < _numOfRounds; ++round)
            {
                //Create a turn record
                Turn turn = new Turn(numOfAgents);

                //Make a move by the active player
                makeMove(round, activePlayer, turn, ref _accumUtility);

                //Choose the next performer
                nextActivePlayer = chooseNextPerformer(activePlayer, inPlayer0ChoiceOfNextPlayer);

                //Update the last observation of each player with the next player
                foreach (Agent agent in _environment.agents)
                {
                    bool actExpected = agent.updateLastObservationWithNextAgent(activePlayer, nextActivePlayer);
                    turn.addExpectedAct(actExpected);
                }

                //Handle the results file
                Results.IncreasePlayerNumberOfTurnsPlayed(activePlayer);
                Results.IncreasePlayerAccumilatingProfit(activePlayer, _accumUtility - _currentUtility);
                _currentUtility = _accumUtility;
                //Set active player
                activePlayer = nextActivePlayer;

                //Log: turn data
                logger.WriteLine(MessageType.Turn, turn);
            }

            /***** Logging the agents' summaries *****/

            return logStatistics(logger);
        }

        /// <summary>
        /// Calculates statistics of the game
        /// </summary>
        /// <param name="logger">Logger for game statistics</param>
        /// <returns></returns>
        protected virtual double logStatistics(Logger logger)
        {
            //Log observations of each agent
            foreach (Agent agent in _environment.agents)
            {
                logger.WriteLine(MessageType.ObservationsHeader, agent.ID);
                agent.ObservationsSet.printObservations(1, 50);
            }

            //Calculate the average utility
            double avgUtility = _accumUtility / _numOfRounds;

            //Log game statistics
            //logger.WriteLine(MessageType.GameStats, AdaptiveAgents.epsilon, _accumUtility, _numOfRounds, avgUtility);

            //Log num of actions
            foreach (Agent agent in _environment.agents)
            {
                logger.WriteLine(MessageType.NumActions, agent.NumActions);
            }

            //Log a new line
            logger.WriteLine(MessageType.LineSeperator, "");

            return avgUtility; //Return the calculated average utility
        }

        public double GetAccumUtility()
        {
            return _accumUtility;
        }
    }
}
